Skip to content

chore: switch to module ESNext + moduleResolution bundler#2095

Open
mattzcarey wants to merge 3 commits into
mainfrom
chore/bundler-tsconfig
Open

chore: switch to module ESNext + moduleResolution bundler#2095
mattzcarey wants to merge 3 commits into
mainfrom
chore/bundler-tsconfig

Conversation

@mattzcarey
Copy link
Copy Markdown
Contributor

Summary

  • Flip the shared common/tsconfig/tsconfig.json from module: NodeNext / moduleResolution: NodeNext to module: ESNext / moduleResolution: bundler.
  • Also flip the two Node16 overrides in examples/client-quickstart/tsconfig.json and examples/server-quickstart/tsconfig.json.
  • Strip .js extensions from every relative TypeScript import across packages/, examples/, scripts/, and test/.
  • Update CLAUDE.md to reflect the new import convention.

Why

Under moduleResolution: NodeNext, every relative TS import has to be written with a .js extension (import x from './foo.js') — a long-standing footgun for new contributors and a frequent source of confusion when scripts/codemods generate imports. moduleResolution: bundler treats the path as a module reference, matching what tsdown, vitest, and downstream consumers' bundlers already do at runtime.

This is an internal-only build configuration change. Consumers are not affected: they continue to import from the built .mjs/.d.mts files declared in each package's exports map, which still carry the .js extensions Node's NodeNext resolver requires at runtime.

Test plan

  • pnpm typecheck:all passes
  • pnpm lint:all passes (including sync:snippets --check)
  • pnpm test:all passes
  • pnpm build:all passes
  • CI green

Internal-only build configuration change. Consumers are not affected:
they continue to import from the built `.mjs`/`.d.mts` files declared
in each package's `exports` map.

What changed:
- `common/tsconfig/tsconfig.json`: `module: NodeNext` → `module: ESNext`,
  `moduleResolution: NodeNext` → `moduleResolution: bundler`.
- `examples/{client,server}-quickstart/tsconfig.json`: same flip
  (they extend a different base and overrode the resolution).
- Strip `.js` extensions from every relative TypeScript import across
  packages/, examples/, scripts/, test/.
- Update CLAUDE.md to reflect the new import convention.

Why:
- Removes the long-standing footgun of having to write `from './foo.js'`
  in `.ts` source files. Bundler resolution treats the path as a module
  reference and lets the tooling resolve it.
- Aligns with what the bundler (`tsdown`), vitest, and downstream
  consumers' bundlers actually do at runtime.

Verification: `pnpm typecheck:all`, `pnpm lint:all`, `pnpm test:all`,
`pnpm build:all` all pass.
@mattzcarey mattzcarey requested a review from a team as a code owner May 15, 2026 14:38
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 15, 2026

⚠️ No Changeset found

Latest commit: c49f054

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 15, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2095

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2095

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2095

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2095

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2095

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2095

commit: 6307e1e

Comment thread examples/client-quickstart/tsconfig.json Outdated
Comment thread packages/client/test/client/middleware.test.ts Outdated
- Make examples/{client,server}-quickstart extend @modelcontextprotocol/tsconfig
  instead of carrying standalone compilerOptions. Drop the inline target/lib/
  module/moduleResolution/strict/etc. duplicates; only outDir, rootDir,
  declaration overrides, and workspace paths remain.
- Align docs/{client,server}-quickstart.md tsconfig snippets to ESNext/bundler
  to match what the example projects now compile under.
- Adjust examples/client-quickstart/src/index.ts for the stricter
  noUncheckedIndexedAccess inherited from the shared base.
- Strip leftover .js suffixes from vi.mock / vi.importActual string specifiers
  in packages/client/test/client/middleware.test.ts.
Comment thread examples/client-quickstart/tsconfig.json
The shared @modelcontextprotocol/tsconfig sets types: [node, vitest/globals],
which the quickstarts inherit even though neither declares vitest as a
devDependency — tsc only finds vitest types via the hoisted workspace root
node_modules. Override types: [node] on each quickstart compilerOptions to
break the accidental coupling.
Comment thread docs/client-quickstart.md
Comment on lines 91 to 92
"target": "ES2023",
"lib": ["ES2023"],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 After consolidating the quickstart tsconfigs onto @modelcontextprotocol/tsconfig, the examples now inherit target: esnext / lib: [esnext] from the shared base, but docs/client-quickstart.md still shows target: ES2023 / lib: [ES2023] and docs/server-quickstart.md still shows target: ES2022 (no lib). This is the same kind of doc/tsconfig drift the PR already fixed for module/moduleResolution — either bump the docs to esnext or pin target/lib overrides in the example tsconfigs to keep them aligned with what the docs advertise.

Extended reasoning...

What the bug is

Before this PR, examples/client-quickstart/tsconfig.json and examples/server-quickstart/tsconfig.json were standalone configs that matched the tsconfig.json blocks in their respective quickstart docs field-for-field on target/lib (client: ES2023/[ES2023]; server: ES2022/no lib). After this PR, both example tsconfigs were rewritten to "extends": "@modelcontextprotocol/tsconfig" and dropped their own target/lib entries. The shared base at common/tsconfig/tsconfig.json:3-4 sets "target": "esnext" and "lib": ["esnext"], and the example overrides only touch outDir, rootDir, declaration, declarationMap, types, and paths — not target/lib. So both examples now compile with an effective target of esnext, while the docs still tell users to use ES2023 / ES2022.

The specific code path

  • docs/client-quickstart.md:91-92"target": "ES2023", "lib": ["ES2023"]
  • examples/client-quickstart/tsconfig.json → extends @modelcontextprotocol/tsconfig, no target/lib override → effective esnext/[esnext]
  • docs/server-quickstart.md:106"target": "ES2022", no lib
  • examples/server-quickstart/tsconfig.json → extends @modelcontextprotocol/tsconfig, no target/lib override → effective esnext/[esnext]

This is a new drift introduced by the PR. The PR already fixed the module/moduleResolution drift in the same doc blocks (raised in an earlier review comment), but the consolidation onto the shared base introduced fresh drift on target/lib that wasn't propagated.

Why existing checks don't catch it

The tsconfig.json blocks in both docs are plain ```json fences with no source= attribute, so `pnpm sync:snippets --check` does not compare them against the example files. The `.ts` code snippets in the docs are `source=`-tagged and synced from the example sources — which means CI typechecks those snippets under the example's effective `target: esnext`, while the doc tells the user to compile with `ES2023`/`ES2022`.

Step-by-step proof

  1. Open common/tsconfig/tsconfig.json — lines 3-4 set "target": "esnext", "lib": ["esnext"].
  2. Open examples/client-quickstart/tsconfig.json after this PR — it has "extends": "@modelcontextprotocol/tsconfig" and its compilerOptions override sets outDir, rootDir, declaration, declarationMap, types, paths but not target or lib. Effective target is therefore esnext.
  3. Open docs/client-quickstart.md:91-92 — it still shows "target": "ES2023", "lib": ["ES2023"].
  4. Same chain for the server quickstart: examples/server-quickstart/tsconfig.json inherits esnext, but docs/server-quickstart.md:106 shows "target": "ES2022" with no lib.
  5. Before the PR (git show HEAD~3:examples/client-quickstart/tsconfig.json), the example was standalone with "target": "ES2023", "lib": ["ES2023"] — identical to the doc block. The drift is introduced here.

Impact

No runtime breakage today — the quickstart code uses no post-ES2022/ES2023 syntax. The latent risk is that CI now typechecks the doc-synced .ts snippets under target: esnext, so a future edit using post-ES2023 syntax (e.g., a new ECMAScript proposal) would compile cleanly in CI but fail for a user who copies the doc tsconfig.json verbatim. It's primarily a documentation-consistency issue, the same class as the module/moduleResolution drift already addressed in this PR.

How to fix

Either:

  • Update the doc tsconfig.json blocks to "target": "esnext" / "lib": ["esnext"] so they match the example's effective config, or
  • Add "target": "ES2023" / "lib": ["ES2023"] (and "target": "ES2022") overrides back into the example compilerOptions so the example matches what the docs advertise.

Optionally, give the doc tsconfig blocks a source= attribute (or similar) so sync:snippets can catch this class of drift going forward.

Comment thread scripts/cli.ts
Comment on lines +2 to +8
import { Client } from '../src/client/index';
import { SSEClientTransport } from '../src/client/sse';
import { StdioClientTransport } from '../src/client/stdio';
import { Server } from '../src/server/index';
import { SSEServerTransport } from '../src/server/sse';
import { StdioServerTransport } from '../src/server/stdio';
import { ListResourcesResultSchema } from '../src/types';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Pre-existing dead code: every relative import in scripts/cli.ts (lines 2–8) points at ../src/* paths that no longer exist post-monorepo (source now lives under packages/{client,server,core}/src/), and the file is not included in any tsconfig nor reachable from the npm scripts that reference it. Rather than continuing to mass-edit an unbuildable file via codemod, consider deleting scripts/cli.ts and the dangling server/client npm scripts in packages/{client,core,server}/package.json that point at it.

Extended reasoning...

What the bug is

The codemod that strips .js extensions touched scripts/cli.ts (lines 2–8), but every relative import in that file points at paths that ceased to exist when the repo was restructured into a monorepo:

import { Client } from '../src/client/index';
import { SSEClientTransport } from '../src/client/sse';
import { StdioClientTransport } from '../src/client/stdio';
import { Server } from '../src/server/index';
import { SSEServerTransport } from '../src/server/sse';
import { StdioServerTransport } from '../src/server/stdio';
import { ListResourcesResultSchema } from '../src/types';

There is no <repo>/src/ directory. Source now lives under packages/{client,server,core}/src/. Several of the imported symbols (SSEServerTransport) don't exist anywhere in packages/ either — the server-side legacy SSE transport was removed.

Why nothing catches it

  • No tsconfig sees the file. There is no root tsconfig.json, and no package tsconfig has scripts/ in its include. pnpm typecheck:all walks per-package configs and never visits scripts/cli.ts.
  • The npm scripts that reference it can't run it. packages/client/package.json:78-79, packages/core/package.json:48-49, and packages/server/package.json:81-82 all declare scripts like tsx scripts/cli.ts server / tsx scripts/cli.ts client — but those run from the package directory, which has no scripts/ subdirectory. The only scripts/cli.ts is at the repo root.

So the file is simultaneously unbuildable, untypechecked, and unreachable.

What this PR did to it

The codemod blindly stripped .js from each of the seven imports (the only change in the diff for this file). This neither fixes nor worsens anything — '../src/client/index.js' and '../src/client/index' are both nonexistent — but it adds churn to the diff and gives the file a false appearance of being maintained.

Step-by-step verification

  1. ls <repo>/srcNo such file or directory. The base ../src/ of every import in the file doesn't exist.
  2. grep -r 'SSEServerTransport' packages/ → no matches. Even the symbol on line 6 was removed in the v2 rewrite.
  3. No <repo>/tsconfig.json exists; the project's tsconfigs are per-package and none lists scripts/.
  4. packages/client/scripts/, packages/core/scripts/, packages/server/scripts/ → none exist, so tsx scripts/cli.ts ... from any of those package dirs fails with Cannot find module.

Impact

None at runtime — the file was already dead and stays dead. The cost is maintenance noise: future codemods will keep touching it, and a contributor reading the diff might assume scripts/cli.ts is a working CLI.

Suggested fix

Delete scripts/cli.ts and the three dangling server/client npm scripts that point at it (in packages/client/package.json, packages/core/package.json, packages/server/package.json). This is pre-existing cleanup, not a regression introduced by this PR — but since the codemod touched the file anyway, it's a natural moment to remove it instead of continuing to edit a corpse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant